import Cesium, { Cartesian3, Rectangle } from '../Ces/Cesium'

import ViewPoint from './ViewPoint'
import We from './We'

let _cartesian2Scratch
let _cartographicScratch

class Util {
  static getFullscreenQuad() {
    const attributes = new Cesium.GeometryAttributes()
    attributes.position = new Cesium.GeometryAttribute({
      componentDatatype: Cesium.ComponentDatatype.FLOAT,
      componentsPerAttribute: 2,
      //  v3----v2
      //  |     |
      //  |     |
      //  v0----v1
      values: new Float32Array([
        -1,
        -1, // v0
        1,
        -1, // v1
        1,
        1, // v2
        -1,
        1, // v3
      ]),
    })

    attributes.st = new Cesium.GeometryAttribute({
      componentDatatype: Cesium.ComponentDatatype.FLOAT,
      componentsPerAttribute: 2,
      values: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
    })

    const fullscreenQuad = new Cesium.Geometry({
      attributes,
      indices: new Uint32Array([3, 2, 0, 0, 2, 1]),
    })
    return fullscreenQuad
  }

  static createTexture(options, typedArray = null) {
    if (Cesium.defined(typedArray)) {
      // typed array needs to be passed as source option, this is required by Cesium.Texture
      const source = {
        arrayBufferView: null,
      }

      source.arrayBufferView = typedArray
      options.source = source
    }

    return new Cesium.Texture(options)
  }

  static createFramebuffer(context, colorTexture, depthTexture = null) {
    const framebuffer = new Cesium.Framebuffer({
      context,
      colorTextures: [colorTexture],
      depthTexture,
    })

    return framebuffer
  }

  static createRawRenderState(options) {
    const translucent = true
    const closed = false
    const existing = {
      viewport: options.viewport,
      depthTest: options.depthTest,
      depthMask: options.depthMask,
      blending: options.blending,
    }

    return Cesium.Appearance.getDefaultRenderState(translucent, closed, existing)
  }

  static viewRectangleToLonLatRange(viewRectangle) {
    const range: any = {}

    const postiveWest = Cesium.Math.mod(viewRectangle.west, Cesium.Math.TWO_PI)
    const postiveEast = Cesium.Math.mod(viewRectangle.east, Cesium.Math.TWO_PI)
    const width = viewRectangle.width

    let longitudeMin

    let longitudeMax

    if (width > Cesium.Math.THREE_PI_OVER_TWO) {
      longitudeMin = 0.0
      longitudeMax = Cesium.Math.TWO_PI
    } else if (postiveEast - postiveWest < width) {
      longitudeMin = postiveWest
      longitudeMax = postiveWest + width
    } else {
      longitudeMin = postiveWest
      longitudeMax = postiveEast
    }

    range.lon = {
      min: Cesium.Math.toDegrees(longitudeMin),
      max: Cesium.Math.toDegrees(longitudeMax),
    }

    const south = viewRectangle.south
    const north = viewRectangle.north
    const height = viewRectangle.height

    const extendHeight = height > Cesium.Math.PI / 12 ? height / 2 : 0

    let extendedSouth = Cesium.Math.clampToLatitudeRange(south - extendHeight)

    let extendedNorth = Cesium.Math.clampToLatitudeRange(north + extendHeight)

    // extend the bound in high latitude area to make sure it can cover all the visible area
    if (extendedSouth < -Cesium.Math.PI_OVER_THREE) {
      extendedSouth = -Cesium.Math.PI_OVER_TWO
    }
    if (extendedNorth > Cesium.Math.PI_OVER_THREE) {
      extendedNorth = Cesium.Math.PI_OVER_TWO
    }

    range.lat = {
      min: Cesium.Math.toDegrees(extendedSouth),
      max: Cesium.Math.toDegrees(extendedNorth),
    }

    return range
  }

  static createRadarTexture(context, radius, ffx) {
    const imageWidth = radius * 2
    const imageHeight = radius * 2
    const lineWidth = ffx < 3 ? 4 : 6
    const fontSize = ffx < 3 ? 18 : 24

    const c = document.createElement('canvas')

    c.width = imageWidth
    c.height = imageHeight
    const ctx = c.getContext('2d')

    ctx.lineWidth = lineWidth
    ctx.strokeStyle = '#FFFFFF'
    ctx.fillStyle = '#FFFFFF'

    const centerX = imageWidth * 0.5
    const centerY = imageHeight * 0.5

    ctx.beginPath()
    ctx.moveTo(centerX, lineWidth)
    ctx.lineTo(centerX, imageHeight - lineWidth)
    ctx.moveTo(lineWidth, centerY)
    ctx.lineTo(imageWidth - lineWidth, centerY)
    ctx.arc(centerX, centerY, radius - lineWidth, 0, 2 * Math.PI)
    ctx.stroke()

    let r = 0

    let rText = 0

    for (let i = 0; i < 7; i++) {
      ctx.beginPath()
      r += (i < 5 ? 50 : 100) * ffx
      rText += i < 5 ? 50 : 100
      ctx.arc(centerX, centerY, r, 0, 2 * Math.PI)
      ctx.stroke()
      ctx.font = i < 5 ? fontSize + 'px serif' : fontSize * 2.0 + 'px serif'
      ctx.lineWidth = i < 5 ? lineWidth : lineWidth * 2

      ctx.translate(centerX, centerX)
      for (let j = 0; j < 4; j++) {
        ctx.strokeStyle = '#000000'
        ctx.strokeText(rText + 'KM', 0, r - lineWidth)
        ctx.fillText(rText + 'KM', 0, r - lineWidth)
        ctx.strokeStyle = '#FFFFFF'
        ctx.rotate(Math.PI / 2)
      }
      ctx.translate(-centerX, -centerX)
    }

    const sampler = new Cesium.Sampler({
      wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
      wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
      minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
      magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR,
    })

    const texture = new Cesium.Texture({
      context,
      source: c,
      sampler,
    })

    return texture
  }

  static clampLineInRectangle(positions, rectangle) {
    if (!Cesium.defined(_cartesian2Scratch)) {
      _cartesian2Scratch = new Cesium.Cartesian2()
    }
    if (!Cesium.defined(_cartographicScratch)) {
      _cartographicScratch = new Cesium.Cartographic()
    }

    const p1 = { x: positions[0], y: positions[1] }
    const p2 = { x: positions[2], y: positions[3] }

    const point = _cartographicScratch

    let maxTestNum = 10000
    const step = 5 / maxTestNum

    point.longitude = p1.x
    point.latitude = p1.y

    if (!Cesium.Rectangle.contains(rectangle, point)) {
      const cartesian2 = _cartesian2Scratch

      cartesian2.x = p2.x - p1.x
      cartesian2.y = p2.y - p1.y
      Cesium.Cartesian2.normalize(cartesian2, cartesian2)

      while (!Cesium.Rectangle.contains(rectangle, point) && maxTestNum-- > 0) {
        point.longitude += cartesian2.x * step
        point.latitude += cartesian2.y * step
      }

      p1.x = point.longitude
      p1.y = point.latitude
    }

    maxTestNum = 10000
    point.longitude = p2.x
    point.latitude = p2.y
    if (!Cesium.Rectangle.contains(rectangle, point)) {
      const cartesian2 = _cartesian2Scratch

      cartesian2.x = p1.x - p2.x
      cartesian2.y = p1.y - p2.y
      Cesium.Cartesian2.normalize(cartesian2, cartesian2)

      while (!Cesium.Rectangle.contains(rectangle, point) && maxTestNum-- > 0) {
        point.longitude += cartesian2.x * step
        point.latitude += cartesian2.y * step
      }

      p2.x = point.longitude
      p2.y = point.latitude
    }

    positions[0] = p1.x
    positions[1] = p1.y
    positions[2] = p2.x
    positions[3] = p2.y
  }

  static computeRectangleMeter = (
    rectangle: Rectangle,
  ): { width: number; height: number } => {
    const ws = new Cesium.Cartographic(rectangle.west, rectangle.south)
    const es = new Cesium.Cartographic(rectangle.west + rectangle.width * 0.25, rectangle.south)
    const wn = new Cesium.Cartographic(rectangle.west, rectangle.south + rectangle.height * 0.25)

    try {
      const geodesic = new Cesium.EllipsoidGeodesic()
      geodesic.setEndPoints(ws, es)
      const width = geodesic.surfaceDistance * 4.0
      geodesic.setEndPoints(ws, wn)
      const height = geodesic.surfaceDistance * 4.0
      return {
        width,
        height,
      }
    } catch (e) {
      console.log(rectangle)
      return {
        width: 1e6,
        height: 1e6,
      }
    }
  }

  static setRefDestroy(obj: any, firstRefCount: number = 0, callback: any = null) {
    const destroyRaw = obj.destroy
    obj.refCount = firstRefCount
    obj.destroy = () => {
      if (--obj.refCount === 0) {
        destroyRaw.bind(obj)()
        if (callback) callback()
      }
    }
  }

  static getEnuModelMatrix(position, result = Cesium.Matrix4.IDENTITY.clone()) {
    if (!(position instanceof Cesium.Cartesian3))
      position = Cesium.Cartesian3.fromDegrees(...position)
    return Cesium.Transforms.eastNorthUpToFixedFrame(position, Cesium.Ellipsoid.WGS84, result)
  }

  static translateMatrix(translate, result = Cesium.Matrix4.IDENTITY.clone()) {
    if (!(translate instanceof Cesium.Cartesian3)) translate = new Cesium.Cartesian3(...translate)
    return Cesium.Matrix4.multiplyByTranslation(result, translate, result)
  }

  static scaleMatrix(scale, result = Cesium.Matrix4.IDENTITY.clone()) {
    if (!(scale instanceof Cesium.Cartesian3)) scale = new Cesium.Cartesian3(...scale)
    return Cesium.Matrix4.multiply(result, Cesium.Matrix4.fromScale(scale), result)
  }

  static rotateMatrix(rotate: number[], result = Cesium.Matrix4.IDENTITY.clone()) {
    Cesium.Matrix4.multiply(
      result,
      Cesium.Matrix4.fromRotation(Cesium.Matrix3.fromRotationZ(Cesium.Math.toRadians(rotate[2]))),
      result,
    )
    Cesium.Matrix4.multiply(
      result,
      Cesium.Matrix4.fromRotation(Cesium.Matrix3.fromRotationY(Cesium.Math.toRadians(rotate[1]))),
      result,
    )
    Cesium.Matrix4.multiply(
      result,
      Cesium.Matrix4.fromRotation(Cesium.Matrix3.fromRotationX(Cesium.Math.toRadians(rotate[0]))),
      result,
    )

    return result
  }

  static We_fantasticJumpToViewpoint = (
    WE: We,
    viewPoint: ViewPoint,
    duration: number,
    animation: {
      animationDelay?: number
      animationHeight?: number
      animationDuration?: number
    },
    callback: any = null,
  ) => {
    const animationDelay = animation.animationDelay?? 0.5
    const animationHeight = animation.animationHeight?? 1e7
    const animationDuration = animation.animationDuration?? 2.3
    const viewPoints = [
      {
        ...viewPoint,
        duration,
      },
      {
        ...viewPoint,
        heading: 0.0,
        roll: 0.0,
        pitch: -Cesium.Math.PI_OVER_TWO,
        height: animationHeight,
        duration: animationDuration,
      },
      {
        ...viewPoint,
        longitude: 0.0,
        heading: 0.0,
        roll: 0.0,
        pitch: -Cesium.Math.PI_OVER_TWO,
        height: animationHeight,
        duration: animationDuration,
      },
    ]

    function complete() {
      if (!WE.handlerManager.enableFlyHandler) return
      if (viewPoints.length > 0) {
        const vp: any = viewPoints.pop()
        WE.jumpToViewpoint(vp, vp.duration, complete)
      } else if (callback) {
        callback()
      }
    }

    WE.jumpToViewpoint(viewPoints.pop(), 0.0, () => {
      setTimeout(() => {
        complete()
      }, animationDelay * 1000)
    })
    WE.handlerManager.enableFlyHandler = true
  }

  static We_lookAround = (WE: We, target: Cartesian3, headingPitchRange, duration, count, callback) => {
    WE.handlerManager.enableFlyHandler = true
    const viewer = WE.viewer

    const startTime = WE.viewer.clock.currentTime.clone()

    const initialHeading = headingPitchRange.heading + 0

    const stepAngle = (Cesium.Math.TWO_PI / duration) * (initialHeading < 0 ? -1 : 1)

    const rotate = () => {
      const delTime =
        Cesium.JulianDate.secondsDifference(viewer.clock.currentTime, startTime) /
        viewer.clock.multiplier
      headingPitchRange.heading = initialHeading + delTime * stepAngle
      viewer.camera.lookAt(target, headingPitchRange)

      if (!WE.handlerManager.enableFlyHandler || (count !== 0 && delTime / duration > count)) {
        viewer.clock.onTick.removeEventListener(rotate)
        viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY)
        if (callback) callback()
      }
    }
    viewer.clock.onTick.addEventListener(rotate)
  }

  static We_travelWorld = (WE: We, options: any = {}) => {
    if (!options) {
      options = {}
    }
    const vp = ViewPoint.clone(WE.viewPoint)

    vp.height = options.height?? 10000000.0
    vp.latitudeD = options.latitude?? 30.0
    const duration = options.duration?? 30.0

    function quadraticIn(time) {
      return time
    }
    function complete1() {
      vp.longitudeD += 179.0
      const flyOverLongitude = vp.longitude

      vp.longitudeD += 180.0

      WE.viewer.camera.flyTo({
        destination: vp.cartesion3,
        duration,
        flyOverLongitude,
        complete: complete1,
        easingFunction: quadraticIn,
      })
    }
    WE.viewer.camera.flyTo({
      destination: vp.cartesion3,
      duration: 0.5,
      complete: complete1,
    })

    WE.handlerManager.enableFlyHandler = true
    WE.handlerManager.flyHander.callback = options.callback
  }

  static We_downloadCanvas = (c: HTMLCanvasElement, file: string | null = null) => {
    c.toBlob(
      (blob: any) => {
        const eleLink = document.createElement('a')
        eleLink.download = file || `${Date.now()}_canvas.png`
        eleLink.style.display = 'none'
        eleLink.href = URL.createObjectURL(blob)
        document.body.append(eleLink)
        eleLink.click()
        eleLink.remove()
      },
      'image/png',
      1,
    )
  }

  static We_downloadObj = (obj: any, file: string | null = null) => {
    let blob
    if (obj.buffer) {
      const arrayBuffer = obj.buffer
      blob = new Blob([arrayBuffer], { type: 'application/octet-stream' })
    } else {
      const str = JSON.stringify(obj)
      blob = new Blob([str], { type: 'application/jsoncharset=utf-8' })
    }

    const eleLink = document.createElement('a')

    eleLink.download = file || `${Date.now()}.json`
    eleLink.style.display = 'none'
    eleLink.href = URL.createObjectURL(blob)
    document.body.append(eleLink)
    eleLink.click()
    eleLink.remove()
  }

  static We_generateIconImagesAtals = (images, makeGray = true) => {
    const imageCount = images.length
    const image = images[0]
    const width = image.width
    const height = image.height
    const maxCol = Math.ceil(Math.sqrt(imageCount))
    const maxRow = Math.ceil(imageCount / maxCol)

    const canvas = document.createElement('canvas')
    canvas.width = width * maxCol * (makeGray ? 2 : 1)
    canvas.height = height * maxRow
    const ctx1 = canvas.getContext('2d', { willReadFrequently: true })

    for (let i = 0; i < imageCount; i++) {
      const imageC = (i % maxCol) * (makeGray ? 2 : 1)
      const imageR = Math.floor(i / maxCol)
      const offsetX = imageC * width
      const offsetY = imageR * height
      ctx1.drawImage(images[i], offsetX, offsetY)
      if (makeGray) {
        const imgData = ctx1.getImageData(offsetX, offsetY, width, height)
        for (let row = 0; row < height; row++) {
          for (let col = 0; col < width; col++) {
            const idx = row * width + col
            const r = imgData.data[idx * 4 + 0]
            const g = imgData.data[idx * 4 + 1]
            const b = imgData.data[idx * 4 + 2]
            const bw = (Math.min(r, Math.min(g, b)) + Math.max(r, Math.max(g, b))) * 0.5
            imgData.data[idx * 4 + 0] = bw
            imgData.data[idx * 4 + 1] = bw
            imgData.data[idx * 4 + 2] = bw
          }
        }
        ctx1.putImageData(imgData, offsetX + width, offsetY)
      }
    }

    return canvas
  }
}

export default Util
